import java.awt.*;
import java.awt.geom.Line2D;
import javax.swing.*;
 
 /** 
 * La classe Arc collega due istanze di Node tra di loro.
 *  
 * @author Massimiliano Ferretti matricola 79769 
 */
 
 public class Arc 
  implements java.io.Serializable  
 {
  private Point dummyPos;
  
  private int weight;
  
  private Node source;
  private Node target;
  private boolean selected;
  private boolean preSelected;
 
  /**
   * Imposta il nodo di destinazione
   * @param target  Istanza delle classe Node destinazione dell'arco   
   */     
  public void setTarget(Node target)
  {
    this.target = target;
  }
  
  /**
   * Restituisce il nodo di destinazione
   * @return  L'istanza di Node destinazione dell'arco   
   */     
  public Node getTarget()
  {
    return target;
  }
  
  /**
   * Restituisce il nodo di origine
   * @return  L'istanza di Node origine dell'arco   
   */     
   public Node getSource()
   {
    return source;
   }
  
  /**
   * Imposta le coordinate del target fittizio. Queste coordinate sostituiscono
   * il target nel caso la proprietà Target sia null. Questa situazione
   * si verifica durante la creazione.
   * @param pos Coordinata del target fittizio
   */     
  public void setTarget(Point pos)
  {
    dummyPos.x = pos.x;
    dummyPos.y = pos.y;
  }
 
  /**
   * Ottiene il peso corrente dell'arco
   * @return Peso dell'arco
   */        
  public int getWeight()
  {
    return weight;
  }               
   
  /**
   * Imposta il nuovo peso dell'arco
   * @param newWeight Nuovo peso dell'arco
   */
  public void setWeight(int newWeight)
  {
    weight = newWeight;
  }     
           
  public boolean intersect(int x, int y)
  {
    if(!isComplete())
      return false;
  
    final double maxDistance = 4.0;
    Line2D.Double line = adjustCoord();
 
    if(x > Math.min(line.x1, line.x2) && x < Math.max(line.x1, line.x2) &&
       y > Math.min(line.y1, line.y2) && y < Math.max(line.y1, line.y2))
    {
        double m = (line.y2 - line.y1) / (line.x2 - line.x1);
        double q = line.y1 - m * line.x1;
        
        double distance = (y - m*x - q) / (Math.sqrt(1 + Math.pow(m, 2)));
        
        return (distance >= -maxDistance && distance <= maxDistance);  
    }
  
    return false;
  } 
   /**
   * Restituisce se l'arco è  correntemente selezionato
   * @return  true se l'arco è selezionato, false altrimenti   
   */     
  public boolean isSelected()
  {
    return selected;
  }
  
  /**
   * Restituisce se l'arco è correntemente in pre-selezione
   * @return true se l'arco è pre-selezionato, false altrimenti   
   */     
   public boolean isPreSelected()
   {
    return preSelected;
   }
  
  /**
   * Controlla se la coordinata passata come parametro rientra nell'area 
   * dell'arco, in caso positivo passa nello stato di pre-selezione.     
   * @param x Coordinata X della posizione del mouse
   * @param y Coordinata Y della posizione del mouse       
   */     
   public void preSelect(int x, int y)
   {
    preSelected = intersect(x, y);
   }  
  
  /**
   * Controlla se la coordinata passata come parametro rientra nell'area
   * dell'arco, in caso positivo passa nello stato di selezione   
   * @param x Coordinata X della posizione del mouse
   * @param y Coordinata Y della posizione del mouse       
   **/     
  public void select(int x, int y)
  {
    selected = intersect(x, y);
  }
 
   /**
   * Costruttore della classe Arc
   * è sufficiente specificare il nodo di origine, finchè non verrà impostato 
   * il nodo di destinazione l'arco sarà in modalità di editing   
   * @param source  Istanza della classe Node dalla quale ha origine l'arco   
   * @see Node
   */     
  public Arc(Node source)
  {
    Point sPos = source.getPos();
    this.dummyPos = new Point(sPos.x, sPos.y);
    this.selected = false;
    this.preSelected = false;
    this.source = source;
    weight = 1;
  }
  
  /**
   * Restituisce se l'arco corrente appartiene ad un cammino minimo 
   * precedentemente calcolato. Se il cammino minimo non è stato ancora 
   * calcolato restituisce, ovviamente, false.
   * Si considera che l'arco appartiene ad un percorso minimo se l'indice 
   * univoco del nodo sorgente è uguale all'indice univoco del precedente
   * del nodo destinazione.         
   * @return true se appartiene ad un percorso minimo, false altrimenti         
   */     
  public boolean isMinimum()
  {
    if(target != null && source != null && target.getPrevious() != null)
        return target.getPrevious().getIndex() == source.getIndex(); 
    
    return false;
  }
  
  /**
   * Restituisce se l'arco corrente è completo, cioè se ha correttamente
   * impostato anche il nodo di destinazione.  
   * @return true se l'arco è completo, false altrimenti     
   */     
  public boolean isComplete()
  {
    return target != null;
  }
  
  private Line2D.Double adjustCoord()
  {
    Point targetPos;
    Point sourcePos;
    
    if(target != null)
      targetPos = target.getPos();
    else
      targetPos = dummyPos;
    
    if(source != null)
      sourcePos = source.getPos();
    else
      sourcePos = new Point(0, 0); // Non dovremmo MAI trovarci in questo caso
    
    double alpha = Math.atan2(targetPos.y - sourcePos.y, 
                              targetPos.x - sourcePos.x);

    double deltaX = Math.cos(alpha) * Node.Radius();
    double deltaY = Math.sin(alpha) * Node.Radius();
    
    if(isComplete())
      return new Line2D.Double(sourcePos.x + Node.Radius() + deltaX, sourcePos.y + Node.Radius() + deltaY,
                               targetPos.x + Node.Radius() - deltaX, targetPos.y + Node.Radius() - deltaY);
    else
      return new Line2D.Double(sourcePos.x + Node.Radius() + deltaX, sourcePos.y + Node.Radius() + deltaY,
                               targetPos.x , targetPos.y);
  }
  
  private Polygon calculateArrow(Line2D.Double line)
  {
    double alpha = Math.atan2 (line.y2 - line.y1, line.x2 - line.x1) + Math.PI;
    final double alphaArrow = 0.5;
    final double arrowLength = 15;

    double x1 = line.x2 + arrowLength * Math.cos(alpha - alphaArrow);
    double y1 = line.y2 + arrowLength * Math.sin(alpha - alphaArrow);
    double x2 = line.x2 + arrowLength * Math.cos(alpha + alphaArrow);
    double y2 = line.y2 + arrowLength * Math.sin(alpha + alphaArrow);
    
    Polygon result = new Polygon();
    result.addPoint((int) x1, (int) y1);
    result.addPoint((int) line.x2, (int) line.y2);
    result.addPoint((int) x2, (int) y2);
    
    return result;
  }
  
  public void paint(Graphics2D g)
  {
    // Salva le impostazioni
    Color defaultColor = g.getColor();
    Stroke defaultStroke = g.getStroke();
    Font defaultFont = g.getFont();
    
    int lineWidth = 1;
    
    if(isMinimum())
      lineWidth = 3;

    if(isComplete())
      g.setStroke(new BasicStroke(lineWidth));
    else
      g.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{9}, 0));
      
    if(isSelected())
      g.setColor(Color.GREEN);
    else if(isPreSelected())
      g.setColor(Color.YELLOW);
    else
      g.setColor(Color.BLACK);
      
    Line2D.Double line = adjustCoord();
    g.draw(line);

    // Disegna la freccia
    if(isComplete())
    {
      Polygon arrow = calculateArrow(line);
      g.setColor(Color.WHITE);
      g.fill(arrow);
      g.setColor(Color.BLACK);
      g.draw(arrow);
     }
     
     // Disegna il circolo sotto il peso
     Point median = new Point((int) (line.x1 + line.x2) / 2, (int) (line.y1 + line.y2) / 2);
     if(isMinimum())
      g.setColor(Color.GREEN);
     else
      g.setColor(Color.GRAY);
     g.fillOval(median.x - 10, median.y - 10, 20, 20);
     
     // Scrive il peso dell'arco
     String textWeight = Integer.toString(weight);
     FontMetrics metrics = g.getFontMetrics(defaultFont);
     int h = metrics.getHeight();
     int w = metrics.stringWidth(textWeight);     
     
     g.setColor(Color.WHITE);
     g.drawString(textWeight, median.x - w/2, median.y + 2);
     
    // Reimposta le impostazioni originali
    g.setFont(defaultFont);
    g.setStroke(defaultStroke);
    g.setColor(defaultColor);
  }          
 }  
